"""
Display
Copyright (C) 2007 Andrew Briscoe

A module used to display a board.
"""

__license__ = 'GPL http://www.gnu.org/licenses/gpl.txt'
__author__ = 'Andrew Briscoe <everynothing@gmail.com>'
__version__ = '0.0.1'
__program__ = 'PIGS'

import wx, os, Game, Chess, Go, RandomStuff, Client, copy

class BoardControl(wx.Frame):
    'A frame representing a board.'
    
    #The name of this boards squares
    SQUARE_IMAGE_NAMES = ['Light', 'Dark']
    #Pieces that neither player owns.
    NEUTRAL_PIECES     = []
    #Piece by players name
    PIECE_BY_PLAYER = None
    #The buttons this board shows
    BUTTONS = ['Resign', 'Offer Draw', 'Request Undo', 'Close']
    #Flip for second player at start?
    AUTOFLIP = False
    
    def __init__(self, control, number, game, player, players):
        
        def make_img(name, typ, bmp, game_path):
            'Image factory'
            
            path = self.img_path
            if game_path:
                path += self.game.board.GAME_TYPE + '/'
            img = wx.Image(path + name + typ, \
                {'.jpg': wx.BITMAP_TYPE_JPEG, '.gif': wx.BITMAP_TYPE_GIF}[typ])
            if bmp:
                img = img.ConvertToBitmap()
            return img
        
        def send_chat(txt):
            "Sends the chat text."
            
            self.send_other(['Chat', 'Game!-> ' + str(self.number), txt])
            
        def send(array):
            "Sends a single command."
            
            self.send_other([array])

        wx.Frame.__init__(self, None, title=game.message(), size=(800, 600))
        self.control = control
        self.number = number
        self.player = player
        self.game = game
        #The last size that this board was resized to.
        self.latest_resize = None
        #The images representing squares when initially loaded.
        #Index board.players is the neutral pieces, that +1 is the squares, 
        #and that +1 is the shading.  Show_pieces has the same ordering.
        self.starting_pieces = [{} for i in xrange(game.players+3)]
        #The bitmaps representing pieces to be drawn, already resized.
        self.show_pieces = [{} for i in xrange(game.players+3)]
        #The bitmaps representing captures to be drawn, already resized (1/3rd).
        self.show_captures = [{} for i in xrange(game.players+3)]
        #The location of this boards images.
        self.img_path = os.path.dirname(os.path.abspath(__file__)) + '/Images/'
        #Which square is selected?  Will be a Board.Coord()
        self.selection = None
        #What squares can the piece selected move to?
        self.possible = []
        #Has the board been reversed?
        self.flipped = False
        
        self.Bind(wx.EVT_CLOSE, self.on_close)
        self.setup_images()
        self.make_menu()
        
        player_panel = wx.BoxSizer(wx.HORIZONTAL)
        self.player_panels = [PlayerPanel(self, c, players[c]) \
                              for c in xrange(game.players)]
        for panel in self.player_panels:
            player_panel.Add(panel, 1, wx.EXPAND | wx.ALL)
        
        buttons = Client.make_buttons(self, [(b, self.actions()[b]) \
                             for b in self.BUTTONS], wx.BoxSizer(wx.VERTICAL))
        self.console = Client.Console(self, self.control, 
                                      'Game!-> ' + str(number))
        
        side_panel = wx.BoxSizer(wx.VERTICAL)
        for add in [(player_panel, 2), (buttons, 2), 
                    (self.console, game.rows - 4)]:
            side_panel.Add(add[0], add[1], wx.EXPAND | wx.ALL)
        
        self.board_panel = BoardPanel(self)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.board_panel, 9, wx.EXPAND | wx.SHAPED)
        sizer.Add(side_panel, 4, wx.GROW)
        self.SetSizer(sizer)
        RandomStuff.fit_center_show(self, False)
        if self.AUTOFLIP and self.player == 1:
            self.flip()
    
    def setup_images(self):
        "Sets up this board controls images."
        
        def make_img(name, typ, bmp, game_path):
            'Image factory'
            
            path = self.img_path
            if game_path:
                path += self.game.board.GAME_TYPE + '/'
            types = {'.jpg': wx.BITMAP_TYPE_JPEG, '.gif': wx.BITMAP_TYPE_GIF}
            img = wx.Image(path + name + typ, types[typ])
            if bmp:
                img = img.ConvertToBitmap()
            return img
        
        #Initialize this boards square images.
        for img_name in self.SQUARE_IMAGE_NAMES:
            row = self.square_row()
            self.starting_pieces[row][img_name] = \
                make_img('Squares/' + img_name, '.jpg', False, True)
            self.show_pieces[row][img_name] = None
            
        #Initializ piece images, a list of dicts of all possible pieces.
        for player, img_name in self.iter_imgs():
            neut = img_name in self.NEUTRAL_PIECES
            ind = self.game.players if neut else player
            color = 'Neutral' if neut else self.game.COLORS[player]
            self.starting_pieces[ind][img_name] = \
                make_img(color + '/' + img_name, '.gif', False, True)
            self.show_pieces[ind][img_name] = None
            self.show_captures[ind][img_name] = None
            
    def make_menu(self):
        "Sets up the menu."
        
        hlp = {'Resign': "Give up.", 'Offer Draw': "Offer a draw.", 
               'Request Undo': 'Ask your opponent for a takeback.', 
               'Pass': 'Skip your turn.', 
               'Done': 'Indicate you are finished scoring.'}
        main = ("&File", [(self.on_close, "Q&uit", "Quit the game.")])
        buts = copy.deepcopy(self.BUTTONS)
        for item in ['Close']:
            if item in buts:
                buts.remove(item)
        game_buts = [(self.actions()[b], '&' + b, hlp[b]) for b in buts]
        game_menu = ("&Game", game_buts)
        visual_opts = ("&Visual Options", [(self.flip, 
                    "F&lip Board", "Rotate the board 180 degrees.")])
        Client.setup_menu(self, [main, game_menu, visual_opts])
        
    def actions(self):
        "What actions can this board control do?"
        
        return {'Resign': self.resign, 'Offer Draw': self.draw_offer, 
               'Request Undo': self.undo_request, 'Pass': self.pass_move, 
               'Done': self.done_scoring, 'Close': self.on_close}
        
    def on_close(self, event=None):
        "What happens when this game is closed."
        
        self.console.basic_close()
        if not self.game.game_over():
            self.control.send_array(['resign'])
        self.control.games.remove(self)
        self.Destroy()
    
    def flip(self, event=None):
        "Flips the board."
        
        self.flipped = not self.flipped
        for place in self.board_panel.pieces:
            place.loc = self.flip_coord(place.loc)
        self.board_panel.force_update()
        self.board_panel.update()
        
    def flip_coord(self, loc):
        "Flips a coordinate."
        
        gam = self.game
        return Game.Coord(gam.rows - 1 - loc.row, 
                          gam.cols - 1 - loc.col)
        
    def resign(self, event=None):
        "Resign the game."
        
        self.send_other(['resign'])
        
    def draw_offer(self, event=None):
        "Offer a draw."
        
        self.send_other(['draw request'])
        
    def undo_request(self, event=None):
        "Request an undo."
        
        self.send_other(['undo request'])
        
    def pass_move(self, event=None):
        "Skip a move."
        
        self.send_action(Game.Pass())
        
    def done_scoring(self, event=None):
        "Finished scoring."
        
        self.send_action(Go.Done())
        
    def square_row(self):
        
        return self.game.players + 1
                
    def send_action(self, action):
        "Sends an action to server."
        
        self.control.send_array(['act', str(action)])
            
    def execute_action(self, action):
        "Execute an action."
        
        self.game.execute_action(action)
        self.update_board()
                
    def send_other(self, array):
        "Sends another command."
        
        self.control.send_array(array)

    def square_clicked(self, square):
        'A player clicked on square.'
        
        if not self.game.game_over() and self.game.board.turn == self.player:
            self.process_click(square.loc)
        
    def update_board(self, everything = True):
        """Board is updated.  If everything is true, 
        an action has been done since last move."""
        
        brd = self.game.board
        if self.selection:
            self.possible = [m.destination for m in \
                 brd.legal_moves(piece = brd[self.selection])]
        if everything:
            self.SetTitle(brd.message())
            for panel in self.player_panels:
                panel.update()
        self.board_panel.update()
    
    def get_square_image(self, loc):
        'What is the name of the square image for self?'
        
        #Default is a light and dark checkerboard pattern.
        return 'Dark' if abs(loc.row - loc.col) % 2 == 0 else 'Light'
    
    def iter_imgs(self, squares=False):
        'Iterates over all images.'
        
        pls = self.game.players
        for player in xrange(pls):
            for img_name in self.game.board.POSSIBLE_PIECES:
                if not img_name in self.NEUTRAL_PIECES:
                    yield (player, img_name)
        for img_name in self.NEUTRAL_PIECES:
            yield (pls, img_name)
        if squares:
            for img_name in self.SQUARE_IMAGE_NAMES:
                yield (pls + 1, img_name)
        
    def set_square_size(self, size):
        'When the image is resized, resize the images to be drawn to screen.'
        
        def resize_img(player, index, divide):
            'Returns img resized to size.'
            
            siz = size / divide
            return self.starting_pieces[player][index].Scale( \
                   siz, siz).ConvertToBitmap()
        
        if not size == self.latest_resize:
            self.latest_resize = size
            for plr, ind in self.iter_imgs(True):
                self.show_pieces[plr][ind] = resize_img(plr, ind, 1)
                self.show_captures[plr][ind] = resize_img(plr, ind, 3)
            if hasattr(self, 'board_panel'):
                self.board_panel.force_update()
                
    def process_click(self, loc):
        'What happens when a square is clicked, override in subclasses.'
        
        pass
    
class PlayerPanel(wx.Panel):
    'Displays the captured pieces.'
    
    def __init__(self, control, color, player):
        
        wx.Panel.__init__(self, control, size = (-1, -1))
        
        self.control = control
        
        self.capture_images = [
           CaptureImg(self, control, color, x) for x in xrange(9)]
        capture_grid = wx.GridSizer(3, 3)
        capture_grid.AddMany([(p, 1, wx.EXPAND | wx.SHAPED) \
                              for p in self.capture_images])
        
        self.player_img = ImgByName(self, control, color)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(wx.StaticText(self, label=player))
        sizer.Add(self.player_img, 1, wx.EXPAND | wx.ALL | wx.SHAPED)
        sizer.Add(capture_grid, 1, wx.EXPAND | wx.SHAPED)
        self.SetSizer(sizer)
        
    def update(self):
        'Refreshes all the components.'
        
        self.player_img.update_image()
        for img in self.capture_images:
            img.update_image()
            
def make_game_frame(control, game, number, player, players):
    "Turn a string into a board control."
    
    str = game.game_type
    if str == 'Go':
        return GoFrame(control, number, game, player, players)
    elif str == 'Chess':
        return ChessFrame(control, number, game, player, players)
        
class BoardPanel(wx.Panel):
    'A panel that shows the board.'
    
    def __init__(self, control):
        'Creates the images, etc.'
        
        wx.Panel.__init__(self, control, style = wx.DEFAULT_FRAME_STYLE)
        
        brd = control.game.board
        #A list of all the squares representing this boards pieces.
        self.pieces = [SquarePanel(self, control, s.position) for s in brd]
        
        self.piece_grid = wx.GridSizer(brd.rows, brd.cols)
        self.piece_grid.AddMany([(p, 0, wx.EXPAND | wx.SHAPED) \
                                 for p in self.pieces])
        self.SetSizerAndFit(self.piece_grid)
    
    def force_update(self):
        'Sets all squares to refresh.'
        
        #Make sure the images refresh
        for place in self.pieces:
            place.last_update += 'Resize'
            
    def update(self):
        'Updates everything.'
        
        for place in self.pieces:
            place.update_image()
            
class SquarePanel(wx.Panel):
    'A clickable square.'
    
    def __init__(self, parent, control, position):
        
        row, col = position.row, position.col
        wx.Panel.__init__(self, parent, style=wx.NO_FULL_REPAINT_ON_RESIZE)
        
        #The BoardFrame that contains self.
        self.control = control
        #Where this square is in parent.game.
        self.loc = Game.Coord(row, col)
        #The background bitmap of this square.
        self.background_bmp = self.control.get_square_image(self.loc)
        #The piece bitmap of this square.
        self.piece_bmp = None
        #The last refresh of this square, so doesnt get refreshed needlessly.
        self.last_update = 'N/A'
        #The color to shade this square, for selection, 
        #last move, or possible move
        self.shading = None
        
        self.Bind(wx.EVT_SIZE, self.on_size)
        self.Bind(wx.EVT_PAINT, self.draw)
        self.Bind(wx.EVT_LEFT_DOWN, self.clicked)
        
    def on_size(self, event=None):
        """Same as update_image, but top left 
        square tells the parent the resize."""
        
        con = self.control
        first = Game.Coord(0, 0)
        if con.flipped:
            first = con.flip_coord(first)
        if self.loc == first:
            con.set_square_size(self.get_size())
        self.update_image()
        
    def get_size(self):
        'Return the size of this square.'
        
        #Add 1 padding so there are no gaps.
        return self.GetClientSizeTuple()[0] + 1
        
    def update_image(self):
        'Updates the image that self displays.'
        
        con = self.control
        sel, loc, brd = con.selection, self.loc, con.game.board
        piece = brd[self.loc]
        self.shading = wx.GREEN_BRUSH \
            if self.loc in brd.last_action_loc() else None
        #Only compute when there is a selection, quicker.
        if sel:
            self.shading = wx.RED_BRUSH if sel == loc else \
                wx.BLUE_BRUSH if loc in con.possible else self.shading
        #The string used to check if this update is the same.
        this_update = str(piece) + str(self.shading)
        #If drawing would do anything
        if not this_update == self.last_update:
            self.last_update = this_update
            #Updates the bitmap of this piece
            self.piece_bmp = None if piece.BLANK \
                else con.show_pieces[piece.color][piece.TYPE]
            self.draw()
                
    def draw(self, event=None):
        'Draws this squares image.'
        
        dc = wx.PaintDC(self)
        bmp = self.control.show_pieces[self.control.square_row()][self.background_bmp]
        if bmp:
            dc.DrawBitmap(bmp, -1, -1, False)
            self.draw_box(dc, self.shading)
            if self.piece_bmp:
                dc.DrawBitmap(self.piece_bmp, -1, -1, True)
            
    def draw_box(self, dc, brush):
        'Draws a rectangle around this square, on dc.'
        
        s = self.get_size()
        if brush:
            dc.SetBrush(brush)
            dc.DrawRectangleList([[0, 0, s, 6], [0, 0, 6, s], 
                                  [0, s - 6, s, 6], [s - 6, 0, 6, s]])
        
    def clicked(self, event=None):
        'When a player clicks here tell parent this.'
        
        self.control.square_clicked(self)
        
class ImgByName(SquarePanel):
    'An image by a players name'
    
    def __init__(self, parent, control, color):
        
        loc = 200 + color
        self.color = color
        self.background = wx.Color(150, 150, 150)
        SquarePanel.__init__(self, parent, control, Game.Coord(loc, loc))
        
    def update_image(self):
        'The background changes instead of the piece'
        
        self.draw()
        
    def draw(self, event=None):
        'Shows the image.'
        
        con, dc, siz = self.grab_vars(wx.PaintDC(self))
        if self.color == self.control.game.turn():
            self.draw_box(dc, wx.RED_BRUSH)
        piece = con.show_pieces[self.color][con.PIECE_BY_PLAYER]
        corner = (siz - piece.GetHeight()) / 2
        if piece:
            dc.DrawBitmap(piece, corner, corner, True)
    
    def grab_vars(self, dc):
        'What the method name says :).'
        
        dc.Clear()
        siz = self.get_size()
        dc.SetBrush(wx.Brush(self.background))
        dc.DrawRectangle(0, 0, siz, siz)
        return (self.control, dc, siz)
        
    def clicked(self, event=None):
        'Clicking does nothing'
        
        pass
    
class CaptureImg(ImgByName):
    'An image by a players name'
    
    def __init__(self, parent, control, color, index):
        
        self.place = index
        ImgByName.__init__(self, parent, control, color)
        self.background = wx.Color(200, 200, 200)
        
    def draw(self, event=None):
        'Shows the image.'
        
        con, dc, siz = self.grab_vars(wx.PaintDC(self))
        brd = con.game.board
        cap = brd.captures[self.color].captures
        if len(cap) > self.place:
            piece = con.show_captures[brd._next_player(self.color)] \
                [cap[self.place][0]]
            if piece:
                corner = (siz - piece.GetHeight()) / 2
                dc.DrawBitmap(piece, corner, corner, True)
                ammt = cap[self.place][1]
                if ammt > 1:
                    dc.DrawText(str(cap[self.place][1]), 0, 0)
    
class ChessFrame(BoardControl):
    'How Chess is displayed.'
    
    NEUTRAL_PIECES = ['Mountain']
    PIECE_BY_PLAYER = 'King'
    AUTOFLIP = True
        
    def process_click(self, loc):
        'What happens when a player clicks here.'
        
        brd = self.game.board
        #Does the piece clicked have any moves?
        any_moves = brd.any_moves(coord=brd[loc])
        #Was the square clicked the square selected?
        clicked_sel = self.selection == loc
        if self.selection:
            move = brd.gen_move(self.selection, loc)
            self.selection = None
            if move:
                self.send_action(move)
            elif any_moves and not clicked_sel:
                self.selection = loc
        elif any_moves:
            #Select the piece clicked on
            self.selection = loc
        self.update_board(False)
        
class GoFrame(BoardControl):
    'How Go is displayed.'
    
    #The names of the squares on a go board.
    SQUARE_IMAGE_NAMES = ['Normal', 'Hoshi', 'Top', 'Bottom', 'Left', \
        'Right', 'TopLeft', 'TopRight', 'BottomLeft', 'BottomRight']
    PIECE_BY_PLAYER = 'Stone'
    BUTTONS = ['Resign', 'Request Undo', 'Pass', 'Done']
        
    def process_click(self, loc):
        'What happens when a player clicks here.'
        
        self.send_action(self.game.board.gen_move(loc))
    
    def get_square_image(self, loc):
        'What is the name of the square image for loc?'
        
        #x is the row, y the col, s the last row, and ret is what to show.
        x, y, s = loc.row, loc.col, self.game.rows - 1
        
        top    = 'TopLeft' if y == 0 else 'TopRight' if y == s else 'Top'
        left   = 'BottomLeft' if x == s else 'Left'
        bottom = 'BottomRight' if y == s else 'Bottom'
        
        #d is how far the hoshi stones are from the sides.
        d = s / 6 if s > 13 else s / 4
        is_hoshi = s % 2 != 1 and all([b == d or b == s - d or \
           b == s / 2 for b in [x, y]])
        
        return top if x == 0 else left if y == 0 else bottom \
            if x == s else 'Right' if y == s else \
            'Hoshi' if is_hoshi else 'Normal'